﻿
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using NoNameGoServer.Client.Commands;
using NoNameGoServer.Client.Extensions;

namespace NoNameGoServer.Client {
    public class NNGSClient : IDisposable {
        public static readonly TimeSpan INFINITE = new TimeSpan(0, 0, 0, 0, -1);
        private const string MATCH_REQUEST_REGEX = "9 Match\\[.*?\\] in .*? minut.*? requested with .*? as .*?\\.\n9 Use <match (.*?) (.) (.*?) (.*?) (.*?)> or <decline .*?> to respond\\.\n2";
        private const string MATCH_CREATED_REGEX = "9 Creating match \\[(.*?)\\] with .*?\\.";
        private const string MOVE_MADE_REGEX = "15 Game (.*?) I: (.*?) \\((.*?)\\) vs (.*?) \\((.*?)\\)\n15 (.*?)\\((.*?)\\): (.*?)\n";
		private const string SCORE_REGEX = @"20 (.*?) \(W:O\): (.*?) to (.*?) \(B:#\): (.*?)\n";
        private readonly Socket _socket;
        private StringBuilder _receiveBuffer;
        private Queue<Command> _commandQueue;
        private readonly Queue<Error> _errorQueue = new Queue<Error>();
        public const string LETTERS = "ABCDEFGHJKLMNOPQRSTUVWXYZ";

        public class DataReceivedData {
            public byte[] Buffer { get; set; }
            public int Count { get; set; }
        }
        public delegate void DataReceivedDelegate(NNGSClient source, DataReceivedData args);
        public event DataReceivedDelegate DataReceived;

        public delegate void MatchRequestReceivedDelegate(NNGSClient sender, MatchData matchData);
        public event MatchRequestReceivedDelegate MatchRequestReceived;

        public delegate void MatchCreatedReceivedDelegate(NNGSClient sender, int matchNumber);
        public event MatchCreatedReceivedDelegate MatchCreatedReceived;

        public delegate void MoveMadeReceivedDelegate(NNGSClient sender, MoveMadeData moveMadeData);
        public event MoveMadeReceivedDelegate MoveMadeReceived;

        public delegate void CaptureMadeReceivedDelegate(NNGSClient sender, CaptureMadeData captureMadeData);
        public event CaptureMadeReceivedDelegate CaptureMadeReceived;

		public delegate void PassReceivedDelegate(NNGSClient sender, PassReceivedData passReceivedData);
		public event PassReceivedDelegate PassReceived;

		public delegate void ScoreReceivedDelegate(NNGSClient sender, ScoreData scoreData);
		public event ScoreReceivedDelegate ScoreReceived;

        public NNGSClient() {
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        public void ConnectAsync(string hostName, int port, string userName, string password) {
            ConnectPrivate(hostName, port, userName, password);
        }

        /// <summary>
        /// <see cref="WaitHandle.WaitOne(TimeSpan,bool)"/> for description of timeout and return value.
        /// </summary>
        public bool Connect(string hostName, int port, string userName, string password, TimeSpan timeout) {
            ClearErrorQueue();
            EventWaitHandle waitHandle = ConnectPrivate(hostName, port, userName, password);
            bool result = waitHandle.WaitOne(timeout, false);
            ThrowErrorsFromQueue();
            return result;
        }

        /// <summary>
        /// <see cref="WaitHandle.WaitOne(TimeSpan,bool)"/> for description of return value.
        /// </summary>
        public bool Connect(string hostName, int port, string userName, string password) {
            return Connect(hostName, port, userName, password, INFINITE);
        }

        private EventWaitHandle ConnectPrivate(string hostName, int port, string userName, string password) {
            IPAddress[] address = Dns.GetHostAddresses(hostName);
            IPEndPoint remoteEndPoint = new IPEndPoint(address[0], port);
            _commandQueue = new Queue<Command>();
            _receiveBuffer = new StringBuilder();
            EnqueueCommandPrivate(new UserNameCommand(userName));
            EnqueueCommandPrivate(new PasswordCommand(password));
            EventWaitHandle waitHandle = EnqueueCommandPrivate(new SetVariableCommand("client", "true"));
            _socket.BeginConnect(remoteEndPoint, Connected, null);
            return waitHandle;
        }


        public EventWaitHandle EnqueueCommand(Command command) {
            if (!_socket.Connected) {
                throw new NotConnectedException();
            }
			ThrowErrorsFromQueue();
            return EnqueueCommandPrivate(command);
        }

        /// <summary>
        /// <see cref="WaitHandle.WaitOne(TimeSpan,bool)"/> for description of timeout and return value.
        /// </summary>
        public bool EnqueueCommandAndWait(Command command, TimeSpan timeout) {
            ClearErrorQueue();
            EventWaitHandle waitHandle = EnqueueCommand(command);
            bool result = waitHandle.WaitOne(timeout, false);
            ThrowErrorsFromQueue();
            return result;
        }

        private void ThrowErrorsFromQueue() {
        	if (_errorQueue.Count == 1) {
                Error error = _errorQueue.Dequeue();
                throw error.GetException();
            }
        	if (_errorQueue.Count > 1) {
        		MultipleErrorsError error = new MultipleErrorsError();
        		while (_errorQueue.Count > 0) {
        			error.Add(_errorQueue.Dequeue());
        		}
        		throw error.GetException();
        	}
        }

    	public void ClearErrorQueue() {
            _errorQueue.Clear();
        }

        /// <summary>
        /// <see cref="WaitHandle.WaitOne(TimeSpan,bool)"/> for description of return value.
        /// </summary>
        public bool EnqueueCommandAndWait(Command command) {
            return EnqueueCommandAndWait(command, INFINITE);
        }

        private EventWaitHandle EnqueueCommandPrivate(Command command) {
            command.Completed += CommandCompleted;
            _commandQueue.Enqueue(command);

            // only command
            if (_commandQueue.Count == 1) {
                _commandQueue.Peek().Execute(this);
            }
            return command.CompletedEventWaitHandle;
        }

        private void CommandCompleted(Command sender, Command.CompletedArgs args) {
            _commandQueue.Dequeue();
            if (_commandQueue.Count > 0) {
                _commandQueue.Peek().Execute(this);
            }
        }

        private void Connected(IAsyncResult ar) {
            _socket.EndConnect(ar);
            DoReceive();
        }

        private void DoReceive() {
            if (_socket.Connected) {
            	byte[] buffer = new byte[10 * 1024];
            	_socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, DataReceivedFromSocket, buffer);
            }
        }

        private void DataReceivedFromSocket(IAsyncResult ar) {
			if (!_socket.Connected)
				return;

			try {
				byte[] buffer = (byte[])ar.AsyncState;
				int count = _socket.EndReceive(ar);
				if (count > 0) {
					if (DataReceived != null)
						DataReceived(this, new DataReceivedData { Buffer = buffer, Count = count });
					for (int i = 0; i < count; i++) {
						byte b = buffer[i];
						if ((b >= ' ' && b <= '~') || b == '\n') {
							_receiveBuffer.Append((char)buffer[i]);
						}
					}

					ProcessReceiveBuffer();
				}
			} finally {
				DoReceive();
			}
        }

        private void ProcessReceiveBuffer() {
            string line;
            while (_receiveBuffer.Length > 0) {
                line = _receiveBuffer.ToString();
                int eol = line.IndexOf('\n');
                if (eol >= 0) {
                    line = line.Substring(0, eol);
                }

                if (_commandQueue.Count > 0) {
                    bool processed;
                    try {
                        _commandQueue.Peek().ProcessReceivedLine(this, line, out processed);
                    } catch (Exception ex) {
                        processed = false;
                        _errorQueue.Enqueue(new ExceptionError(ex));
                        _commandQueue.Peek().MarkCompleted();
                    }
                    if (processed) {
                        _receiveBuffer.Remove(0, Math.Min(_receiveBuffer.Length, line.Length + 1));
                        continue;
                    }
                }

                if (TryProcessMatchRequest()) {
                    continue;
                }
                if (TryProcessMatchCreated()) {
                    continue;
                }
				if (TryProcessScore()) {
					continue;
				}
                if (TryProcessMoveMade()) {
                    continue;
                }

                _receiveBuffer.Remove(0, Math.Min(_receiveBuffer.Length, line.Length + 1));
            }
        }

		private bool TryProcessScore() {
			string[] groups;
			if (_receiveBuffer.RemoveRegEx(SCORE_REGEX, out groups)) {
				decimal whiteScore = decimal.Parse(groups[2]);
				decimal blackScore = decimal.Parse(groups[4]);
				
				ScoreData scoreData = new ScoreData {
					WhiteScore = whiteScore,
					BlackScore = blackScore
				};

				if (ScoreReceived != null)
					ScoreReceived(this, scoreData);
				return true;
			}

			return false;
		}

        private bool TryProcessMoveMade() {
			string[] groups;
            if (_receiveBuffer.RemoveRegEx(MOVE_MADE_REGEX, out groups)) {
				// ignore handicap commands that look like moves
				if (groups.Length < 9)
					throw new NNGSClientException("It looked like the following response was a move, but apparently it wasn't: '" + _receiveBuffer + "'");

				if (groups[8].Trim().StartsWith("Handicap"))
					return true;

				if (groups[8].Trim().StartsWith("Pass")) {
					if (PassReceived != null) {
						StoneColor color = groups[7] == "B" ? StoneColor.Black : StoneColor.White;
						PassReceivedData passReceivedData = new PassReceivedData {
							Color = color
						};
						PassReceived(this, passReceivedData);
					}
					return true;
				}

				string[] moveAndCaptures = groups[8].Trim().Split(' ');

				int numberOfMove;
				if (!Int32.TryParse(moveAndCaptures[0].Substring(1), out numberOfMove))
					throw new NNGSClientException("The following server response looked like a move but apparently wasn't: '" + _receiveBuffer + "'");

                MoveMadeData moveMadeData = new MoveMadeData {
                    GameNumber = int.Parse(groups[1]),
                    Color = groups[7] == "B" ? StoneColor.Black : StoneColor.White,
                    Letter = moveAndCaptures[0][0],
                    Number = numberOfMove - 1
                };
				if (MoveMadeReceived != null)
					MoveMadeReceived(this, moveMadeData);
                for (int i = 1; i < moveAndCaptures.Length; i++) {
                    CaptureMadeData captureMadeData = new CaptureMadeData {
                        GameNumber = int.Parse(groups[1]),
                        Letter = moveAndCaptures[i][0],
                        Number = int.Parse(moveAndCaptures[i].Substring(1)) - 1
                    };
					if (CaptureMadeReceived != null)
						CaptureMadeReceived(this, captureMadeData);
                }
                return true;
            }
            return false;
        }

        private bool TryProcessMatchCreated() {
            string[] groups;
            if (_receiveBuffer.RemoveRegEx(MATCH_CREATED_REGEX, out groups)) {
                int matchNumber = int.Parse(groups[1]);
				if (MatchCreatedReceived != null)
					MatchCreatedReceived(this, matchNumber);
                return true;
            }
            return false;
        }

        private bool TryProcessMatchRequest() {
            string[] groups;
            if (_receiveBuffer.RemoveRegEx(MATCH_REQUEST_REGEX, out groups)) {
                MatchData matchData = new MatchData {
                    OpponentUserName = groups[1],
                    OpponentColor =
                        groups[2] == "W" ? StoneColor.Black : StoneColor.White,
                    BoardSize = int.Parse(groups[3]),
                    Time = int.Parse(groups[4]),
                    ByoyomiMinutes = int.Parse(groups[5])
                };
				if (MatchRequestReceived != null)
					MatchRequestReceived(this, matchData);
                return true;
            }
            return false;
        }

        public void Dispose() {
            _socket.Close();
        }

        internal void SendAsync(Command src, string str, object state) {
            if (!_socket.Connected) {
                throw new NotConnectedException();
            }

            byte[] buffer = Encoding.ASCII.GetBytes(str);
            SendState sendState = new SendState {
                Command = src,
                State = state
            };
            _socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendComplete, sendState);
        }

        private void SendComplete(IAsyncResult ar) {
			try {
				int count = _socket.EndSend(ar);
				SendState sendState = ar.AsyncState as SendState;
				if (sendState != null) {
					sendState.Command.SendComplete(this, count, sendState.State);
				}
			} catch (Exception) {
				
			}
        }


        private class SendState {
            public Command Command { get; set; }
            public object State { get; set; }
        }
    }
}
